このガイドでは、Sequenceスタックのシンプルなツールを使ってカスタムマーケットプレイスを作成する手順を説明します。 これらのツールで以下のことが可能になります:
  1. ミント:Sequence Builderからウォレットへトークンをミント
  2. ウォレット認証:Web SDKを使ったユーザー認証
  3. Marketplace SDKのセットアップ:Marketplace SDKの基本的な設定ガイド
  4. コレクションの表示:マーケットプレイスで利用可能なコレクションを取得し、表示します。
  5. 出品とオファー:ユーザーがトークンを出品したり、オファーを出せるようにします。

1. ミント

最初のステップは、Sequence Builderでコレクティブルを作成し、いくつかのトークンをミントすることです。詳しくはこちらのガイドを参照し、ミントしたtokenIdを使って今後のステップで注文のクエリや実行を行います。

2. ウォレット認証

プロジェクトには、ユーザーをウォレットで認証する仕組みが必要です。 このガイドでは、GoogleやApple認証に加え、CoinbaseやMetamaskなどユーザー自身のウォレットも利用できる、Embedded WalletWeb SDKコネクタを使用します。

パッケージのインストール

すでに優れたUIと統合されたMarketplace Boilerplateから始めることもできますし、ここではReactをゼロから使う方法も順を追って説明します。 まず、任意のフォルダ名でプロジェクトを作成します。
npm
npm create vite --template react-ts
# or
pnpm create vite --template react-ts
# or
yarn create vite --template react-ts
次に、<project_name>フォルダで必要なパッケージをインストールします。
pnpm
npm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query
# or
pnpm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query
# or
yarn add @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

設定ファイルを作成する

config.ts
import { createConfig } from "@0xsequence/connect";

const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;
const waasConfigKey = import.meta.env.VITE_WAAS_CONFIG_KEY;
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;

export const config: any = createConfig("waas", {
  projectAccessKey,
  chainIds: [1, 137],
  defaultChainId: 1,
  appName: "Demo Dapp",
  waasConfigKey, // for waas
  google: {
    clientId: googleClientId,
  },
  walletConnect: {
    projectId: walletConnectId,
  },
});
使用するすべてのchainIdを追加してください。

main.tsxでレイアウトを作成します。

main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import "./index.css";
import { config } from "./config";
import { SequenceConnect } from "@0xsequence/connect";

function Dapp() {
  return (
    <SequenceConnect config={config}>
      <App />
    </SequenceConnect>
  );
}

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Dapp />
  </StrictMode>,
)

export default Dapp;

認証コンポーネント

認証を完了するには、認証用コンポーネントが必要です。
App.tsx
import { useAccount, useDisconnect } from "wagmi";
import "./App.css";
import { useOpenConnectModal } from "@0xsequence/connect";

function App() {
  const { setOpenConnectModal } = useOpenConnectModal();
  const { isConnected, address } = useAccount();
  const { disconnect } = useDisconnect();

  function handleDisconnect() {
    disconnect();
  }

  if (isConnected && address)
    return (
      <>
        <p>User Address: {address}</p>
        <button onClick={handleDisconnect}>Disconnect</button>
      </>
    );

  return (
    <>
      <button onClick={() => setOpenConnectModal(true)}>Connect</button>
    </>
  );
}

export default App;

3. Marketplace SDKのセットアップ

Marketplace SDKは、マーケットプレイスをアプリケーションにシームレスに統合できる包括的なツールキットです。詳しくはMarketplace SDKをご覧ください。
npm install @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite
# or
pnpm add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite
# or
yarn add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

必要なCSSインポートをメインのスタイルファイルに追加します。

index.css
@import 'tailwindcss';
@import '@0xsequence/marketplace-sdk/styles/preset';
@import '@0xsequence/design-system/preset';

vite.config.tsでTailwind CSSプラグインを設定します。

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Marketplace SDK の設定を追加する

config.ts
import { createConfig } from "@0xsequence/connect";
import type { SdkConfig } from '@0xsequence/marketplace-sdk';

const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;
const waasConfigKey = import.meta.env.VITE_WAAS_CONFIG_KEY;
const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;
const projectId = import.meta.env.VITE_PROJECT_ID!;
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID!;

export const config: any = createConfig("waas", {
  projectAccessKey,
  chainIds: [1, 137],
  defaultChainId: 1,
  appName: "Demo Dapp",
  waasConfigKey, // for waas
  google: {
    clientId: googleClientId,
  },
  walletConnect: {
    projectId: walletConnectId,
  },
});

export const marketplaceConfig = {
        projectId,
        projectAccessKey,
        walletConnectProjectId: walletConnectId
} satisfies SdkConfig;

Marketplace SDK プロバイダーを Web SDK プロバイダーと一緒に追加

main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import "./index.css";
import { config } from "./config";
import { SequenceConnect } from "@0xsequence/connect";
import {
  MarketplaceProvider,
  ModalProvider,
} from "@0xsequence/marketplace-sdk/react";
import { ThemeProvider, ToastProvider } from '@0xsequence/design-system';
import { SequenceCheckoutProvider } from "@0xsequence/checkout";
import { marketplaceConfig } from "./config";

function Dapp() {
  return (
    <ThemeProvider>
      <ToastProvider>
        <SequenceConnect config={config}>
          <SequenceCheckoutProvider>
            <MarketplaceProvider config={marketplaceConfig}>
              <App />
              <ModalProvider />
            </MarketplaceProvider>
          </SequenceCheckoutProvider>
        </SequenceConnect>
      </ToastProvider>
    </ThemeProvider>
  );
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
  <Dapp />
</StrictMode>,
)

export default Dapp;

4. コレクションの表示

マーケットプレイスのコレクションを表示するために、src/components ディレクトリ内に Collections というフォルダを作成します。このフォルダ内に、index.tsxCollection.tsx の2つのファイルを作成します。
Collection.tsx
import type { MarketCollection } from "@0xsequence/marketplace-sdk";
import { useCollection } from "@0xsequence/marketplace-sdk/react";

export const Collection = ({
  collection,
  onSelectCollection,
}: {
  collection: MarketCollection;
  onSelectCollection: (value: MarketCollection) => void;
}) => {
  const { data } = useCollection({
    chainId: collection.chainId,
    collectionAddress: collection.itemsAddress,
  });

  const logoURI = data?.logoURI;
  const image = data?.extensions.ogImage || collection.bannerUrl || logoURI;
  const description = data?.extensions.description;
  const name = data?.name;
  const symbol = data?.symbol;
  const contractType = data?.type;

  function handleOnSelectCollection() {
    onSelectCollection(collection);
  }

  return (
    <div
      onClick={handleOnSelectCollection}
      className="flex flex-col gap-2 border rounded-lg cursor-pointer w-[400px]"
    >
      <h2>
        {name} {symbol && <span>({symbol})</span>}
      </h2>
      <p>{collection.itemsAddress}</p>
      <p>Chain ID: {collection.chainId}</p>
      <img
        src={image}
        alt={collection.itemsAddress}
        className="w-[200px] h-[200px] object-cover object-center"
      />
      <p>Contract type: {contractType}</p>
      <p>{description}</p>
    </div>
  );
};
index.tsx
import type { MarketCollection } from "@0xsequence/marketplace-sdk";
import { Collection } from "./Collection";
import { useMarketplaceConfig } from "@0xsequence/marketplace-sdk/react";

export const Collections = ({ onSelectCollection }: { onSelectCollection: (value: MarketCollection) => void }) => {
    const { data } = useMarketplaceConfig();

    const collections: MarketCollection[] = data?.market.collections || [];
    return (
        <div>
            {collections?.map((collection) => (
                <Collection key={collection.itemsAddress} collection={collection} onSelectCollection={onSelectCollection} />
            ))}
        </div>
    );
};
最後に、App.tsxにCollectionsコンポーネントを追加します。
import { useAccount, useDisconnect } from "wagmi";
import "./App.css";
import { useOpenConnectModal } from "@0xsequence/connect";
import { Collections } from "./components/Collections";
import type { MarketCollection } from "@0xsequence/marketplace-sdk";
import { useState } from "react";

function App() {
  const { setOpenConnectModal } = useOpenConnectModal();
  const { isConnected, address } = useAccount();
  const { disconnect } = useDisconnect();
  const [collectionSelected, setCollectionSelected] =
    useState<MarketCollection | null>(null);

  function handleDisconnect() {
    disconnect();
  }

  function onSelectCollection(value: MarketCollection) {
    setCollectionSelected(value);
  }

  function onRestartSelectedCollectionValue() {
    setCollectionSelected(null);
  }

  if (!isConnected && !address) {
    return (
      <>
        <button onClick={() => setOpenConnectModal(true)}>Connect</button>
      </>
    );
  }

  return (
    <>
      <p>User Address: {address}</p>
      <button onClick={handleDisconnect}>Disconnect</button>
      {!collectionSelected ? (
        <Collections onSelectCollection={onSelectCollection} />
      ) : (
        <>
          <button onClick={onRestartSelectedCollectionValue}>Show Collections</button>
          <div>{JSON.stringify(collectionSelected)}</div>
        </>
      )}
    </>
  );
}

export default App;

5. 出品とオファー

このセクションでは、マーケットプレイスの出品とオファーをUIに統合します。

型定義ファイルの作成

始める前に、src/utils内にコンポーネント用の型をまとめたファイルを用意します。これを使って関数に型を付け、コードベースの一貫性と保守性を高めます。
types.ts
import type {
  MarketplaceKind,
  Order,
  OrderbookKind,
  Step,
} from "@0xsequence/marketplace-sdk";
import type { Address, Hash, Hex } from "viem";

type ModalCallbacks = {
  onSuccess?: ({ hash, orderId }: { hash?: Hash; orderId?: string }) => void;
  onError?: (error: Error) => void;
  onBuyAtFloorPrice?: () => void;
};

export type ShowBuyModalArgs = {
  orderId: string;
  chainId: number;
  collectionAddress: Address;
  collectibleId: string;
  marketplace: MarketplaceKind;
  customCreditCardProviderCallback?: (buyStep: Step) => void;
  skipNativeBalanceCheck?: boolean;
  nativeTokenAddress?: Address;
};

export type ShowCreateListingModalArgs = {
  collectionAddress: Hex;
  chainId: number;
  collectibleId: string;
  orderbookKind?: OrderbookKind;
  callbacks?: ModalCallbacks;
};
export type ShowMakeOfferModalArgs = {
  collectionAddress: Hex;
  chainId: number;
  collectibleId: string;
  orderbookKind?: OrderbookKind;
  callbacks?: ModalCallbacks;
};

export type ShowSellModalArgs = {
  collectionAddress: Hex;
  chainId: number;
  tokenId: string;
  order: Order;
  callbacks?: ModalCallbacks;
};

コレクティブル用コンポーネントのセットアップ

アイテムページおよびそのコンポーネントを作成するために、src/components ディレクトリ内にCollectiblesという新しいフォルダを追加します。このフォルダ内にindex.tsxCollectible.tsxの2つのファイルを作成します。
Collectible.tsx
import type {
    ShowBuyModalArgs,
    ShowCreateListingModalArgs,
    ShowMakeOfferModalArgs,
    ShowSellModalArgs,
} from "../../utils//types";
import type {
    CollectibleOrder,
    MarketplaceKind,
    OrderbookKind,
} from "@0xsequence/marketplace-sdk";
import { useBalanceOfCollectible } from "@0xsequence/marketplace-sdk/react";
import type { Address } from "viem";

export const Collectible = ({
    collectible,
    chainId,
    collectionAddress,
    showBuyModal,
    showListModal,
    showOfferModal,
    showSellModal,
    address,
    isConnected,
    orderbookKind,
}: {
    collectible: CollectibleOrder;
    chainId: string;
    collectionAddress: Address;
    showBuyModal: (args: ShowBuyModalArgs) => void;
    showListModal: (args: ShowCreateListingModalArgs) => void;
    showOfferModal: (args: ShowMakeOfferModalArgs) => void;
    showSellModal: (args: ShowSellModalArgs) => void;
    address?: Address;
    isConnected: boolean;
    orderbookKind: OrderbookKind;
}) => {
    const { name, image, tokenId } = collectible.metadata;

    const { data: userBalanceResp } =
        useBalanceOfCollectible({
            chainId: Number(chainId),
            collectionAddress,
            collectableId: tokenId,
            userAddress: address,
            query: {
                enabled: !!isConnected && !!address,
            },
        });

    const tokenBalance = userBalanceResp?.balance;

    const onClickBuy = () =>
        showBuyModal({
            chainId: Number(chainId),
            collectionAddress,
            collectibleId: tokenId,
            orderId: collectible!.order!.orderId,
            marketplace: orderbookKind as unknown as MarketplaceKind,
        });

    const onClickList = () => {
        showListModal({
            collectionAddress,
            chainId: Number(chainId),
            collectibleId: tokenId,
            orderbookKind,
        });
    };

    const onClickOffer = () => {
        showOfferModal({
            collectionAddress,
            chainId: Number(chainId),
            collectibleId: tokenId,
            orderbookKind,
        });
    };

    const onAcceptOffer = () => {
        showSellModal({
            collectionAddress,
            chainId: Number(chainId),
            tokenId,
            order: collectible!.offer!,
        });
    };

    const hasOffer = Boolean(collectible?.offer);
    const sellDisabled = !isConnected || !hasOffer || !tokenBalance;
    const showActionButtons = address && isConnected;

    return (
        <div className="flex flex-col justify-center items-center border border-white">
            <h2>{name}</h2>
            <img
                src={image}
                alt={name}
                className="w-[200px] h-[200px] object-cover object-center"
            />
            {showActionButtons && (
                <>
                    {collectible.order && <button onClick={onClickBuy}>Buy</button>}
                    {tokenBalance && <button onClick={onClickList}>List</button>}
                    <button onClick={onClickOffer}>Make offer</button>
                    {!sellDisabled && (
                        <button onClick={onAcceptOffer}>Accept Offer</button>
                    )}
                </>
            )}
        </div>
    );
};
index.tsx
import { OrderbookKind, OrderSide } from "@0xsequence/marketplace-sdk";
import {
    useBuyModal,
    useCreateListingModal,
    useListCollectibles,
    useMakeOfferModal,
    useMarketplaceConfig,
    useSellModal,
} from "@0xsequence/marketplace-sdk/react";
import { Collectible } from "./Collectible";
import type { Address } from "viem";
import { useAccount } from "wagmi";

export const Collectibles = ({
    collectionId,
    chainId,
}: {
    collectionId: Address;
    chainId: number;
}) => {
    const { address, isConnected } = useAccount();
    const {
        data: collectibles,
    } = useListCollectibles({
        chainId: Number(chainId),
        collectionAddress: collectionId,
        filter: {
            // # Optional filters
            includeEmpty: true,
            // searchText: text,
            // properties,
        },
        side: OrderSide.listing,
    });

    const { data } = useMarketplaceConfig();

    const onError = (error: Error) => {
        console.error(error.message);
    };

    const { show: showBuyModal } = useBuyModal({
        onSuccess(hash) {
            console.log("Buy transaction sent with hash: ", hash);
        },
        onError,
    });

    const { show: showListModal } = useCreateListingModal({ onError });
    const { show: showOfferModal } = useMakeOfferModal({
        onError,
    });
    const { show: showSellModal } = useSellModal({ onError });

    const collectiblesFlat =
        collectibles?.pages.flatMap((p) => p.collectibles) ?? [];
    const collectionData =
        data?.market.collections?.find(
            (collection) => collection.itemsAddress === collectionId
        ) || null;
    const orderbookKind: OrderbookKind =
        (collectionData?.destinationMarketplace || "") as unknown as OrderbookKind;

    return (
        <div>
            <h1 className="text-[32px] font-semibold">Collectibles</h1>
            {collectiblesFlat?.map((collectible) => (
                <Collectible
                    key={collectible.metadata.tokenId}
                    collectible={collectible}
                    chainId={String(chainId)}
                    collectionAddress={collectionId}
                    showBuyModal={showBuyModal}
                    showListModal={showListModal}
                    showOfferModal={showOfferModal}
                    showSellModal={showSellModal}
                    address={address}
                    isConnected={isConnected}
                    orderbookKind={orderbookKind}
                />
            ))}
        </div>
    );
};
最後に、出品とオファー用のコンポーネントをApp.tsxファイルに追加します。
App.tsx
import { useAccount, useDisconnect } from "wagmi";
import "./App.css";
import { useOpenConnectModal } from "@0xsequence/connect";
import { Collections } from "./components/Collection";
import type { MarketCollection } from "@0xsequence/marketplace-sdk";
import { useState } from "react";
import { Collectibles } from "./components/Collectible/index";
import type { Address } from "viem";

function App() {
  const { setOpenConnectModal } = useOpenConnectModal();
  const { isConnected, address } = useAccount();
  const { disconnect } = useDisconnect();
  const [collectionSelected, setCollectionSelected] =
    useState<MarketCollection | null>(null);

  function handleDisconnect() {
    disconnect();
  }

  function onSelectCollection(value: MarketCollection) {
    setCollectionSelected(value);
  }

  function onRestartSelectedCollectionValue() {
    setCollectionSelected(null);
  }

  if (!isConnected && !address) {
    return (
      <>
        <button onClick={() => setOpenConnectModal(true)}>Connect</button>
      </>
    );
  }

  return (
    <>
      <p>User Address: {address}</p>
      <button onClick={handleDisconnect}>Disconnect</button>
      {!collectionSelected ? (
        <Collections onSelectCollection={onSelectCollection} />
      ) : (
        <>
          <button onClick={onRestartSelectedCollectionValue}>
            Show Collections
          </button>
          <Collectibles
            chainId={collectionSelected.chainId}
            collectionId={collectionSelected.itemsAddress as Address}
          />
        </>
      )}
    </>
  );
}

export default App;

実行とテスト

新しいページにアイテムが正常に追加され、UI上で表示されるようになりました。動作を確認するには、pnpm devを実行してください。ランディングページから任意のコレクションを選択し、その中のNFTを表示できます。出品、購入、オファーの作成や承認など、主要なフローをテストして、すべてが期待通りに動作するか確認しましょう。